Repository navigation
Turn on the desktop native themes, and fix what turning them on exposed - #5861
Conversation
Four things a desktop form needs that the framework either lacked or built and never wired up. **Tab and Shift-Tab move focus.** The traversal machinery is years old -- TabIterator, getNextComponent, preferredTabIndex -- and no key was ever connected to it, so its only callers were the "next field while editing" paths in TextEditUtil and Picker. Connecting Tab to that iterator would have been wrong, and the reason is worth recording: its filter opens with `getTabIndex() >= 0`, Component.preferredTabIndex defaults to -1, and TextArea is the ONLY class in the framework that ever calls setPreferredTabIndex(0). So the iterator holds text areas and nothing else -- correct for next-field-while-editing, and for Tab it would walk between a form's text fields and skip every button, checkbox and slider between them. The desktop order is therefore its own: same walk, same comparator shape, but filtered on focusability, which is what a pointer can reach and so what a keyboard must reach. An explicit preferredTabIndex still sorts first; zero counts as unnumbered rather than as first, because that is what setTraversable(true) writes and reading it as a position would put every TextArea ahead of its own label. Wraps at both ends so the keyboard cannot strand itself. **Escape cancels.** On a Dialog it does what the window's close control does, which was already written twice as "the back command, or dispose when there isn't one" -- now written once, as Dialog.cancel(), with both former sites calling it. On a plain Form it fires the back command and does nothing at all when there is none: Escape must never be able to exit an application. Window honours its close operation, so one that refuses the close button refuses this too. Enter needed nothing: ports already map it to GAME_KEY_CODE_FIRE and keyReleased already fires getDefaultCommand() on GAME_FIRE. Both keys are gated on isDesktop(), and the gate is asserted in both directions -- a gate nobody tests from the other side is not a gate. **desktopTitleBarMode had no reader.** All three desktop native themes have carried this constant since they landed, and nothing ever read it: Form went to Display.impl.getDesktopTitleBarMode(), which is sourced from the build hint. The constant was dead text. It cannot simply be consulted either, because that method is documented to answer a usable mode and so cannot say "nobody asked" -- it answers "toolbar" both for a port with no opinion and for a project that deliberately chose the legacy look. Hence getConfiguredDesktopTitleBarMode(), which reports exactly that distinction: build hint first, then the theme constant, then the port. JavaSEPort's static drops its "toolbar" default for null and coalesces at each reader, which also stops injectDesktopThemeConstants from writing that default over a theme's own constant. **The four interactive-scrollbar UIIDs are seeded.** LookAndFeel.initScroll picks DesktopScroll/DesktopScrollThumb and the horizontal pair when interactiveScrollBool is on, and UIManager.resetThemeProps seeded only the four mobile ones. A theme that turned the constant on without defining all four therefore drew its track and thumb out of the blank default style: an invisible scrollbar, still reserving its gutter, with nothing reporting a problem. Seeded like their mobile counterparts plus the two things the desktop bar needs and the mobile one does not -- a gutter wide enough to grab, and thumb hover/pressed styles so the highlight exists before a theme styles it. Guarded like every other seed here, so the desktop themes still suppress them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… had All three exist in every desktop toolkit and had no Codename One equivalent, which is why a desktop form built out of the existing components reads as one undifferentiated column of controls. **Separator** -- NSBox in separator mode, MenuFlyoutSeparator, GtkSeparator. Applications drew their own out of a Label with a bottom border or a fixed-height Container with a background colour. Those look approximately right on one platform and wrong everywhere else, because what differs between platforms is exactly the part they hard-code: the thickness, the colour and the air either side. All three come from the Separator UIID here -- border where there is one, background colour otherwise, margin for the air, and separatorThicknessMM for the thickness. Never thinner than a pixel: a theme may ask for a hairline, a hairline rounds to zero millimetres worth of pixels on a dense screen, and zero makes the rule invisible rather than thin. Not focusable, so the new desktop traversal skips it. The constant is read as a string like every other *MM constant (the theme format has no float accessor) and a malformed one falls back rather than throwing out of a paint. **GroupBox** -- NSBox with a title, GtkFrame with a label widget, WinUI headered content. Two UIIDs: GroupBox styles the frame and GroupBoxTitle the caption. Nothing hard-codes where the caption sits relative to the top edge, because the three platforms disagree about it; a theme that wants it inset into the edge uses a negative top margin. An empty caption hides the strip entirely rather than leaving a gap nothing explains. Adds route into the content pane, including the constrained form -- the two-slot BorderLayout underneath is an implementation detail, and BorderLayout.SOUTH from a caller means "below the other controls in this group", never "outside the box". **Stepper** -- NSStepper, NumberBox with spin buttons, GtkSpinButton. The nearest existing thing is Slider, which is a different control for a different job: a slider is for a value whose exact number does not matter and a stepper is for one where it does. Every hand-built composite got the same two things wrong, so both are handled here: the field cannot hold a number this control could not produce (out-of-range typing is corrected under the caret, and empty or unparseable text is left alone so clearing the field to retype it does not watch it fill itself back in), and the button that would step past a bound is disabled rather than accepting a press that does nothing. Fires only when the value actually moved. The value is an int, deliberately. A fractional stepper is a real control on some platforms and doing it properly needs a format, a locale and a parse policy; guessing those is worse than not offering them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wo behaviours The three desktop native themes shipped with a scrollbar derived from the mobile one and nothing defined for any surface Codename One still draws itself. Both are fixed here, and both were the kind of gap nothing reports: the controls simply look wrong. **The scrollbar was `cn1-derive: ScrollThumb` and a transparent track.** That is a desktop scrollbar with no gutter, no minimum thumb length and no highlight, in themes whose whole job is to look like the platform -- while the MOBILE themes (ios-modern, android-material) carried the full desktop treatment. Backwards. Each theme now sizes its own gutter from the platform's figure (12px Fluent, 15px Aqua overlay, 13px Adwaita), insets the thumb inside it, and gives the thumb its hover and drag colours. Two things about the states are worth recording, both measured rather than assumed by reverting one theme and reading the compiled `.res` back: - The highlight states are `.selected` and `.pressed`, NOT `.hover`. LookAndFeel's InteractiveScrollThumb returns getSelectedStyle() under the pointer and getPressedStyle() while dragged. A `.hover` rule here compiles and is never painted. - `cn1-derive` emits the whole state family by copying the base, so the old stub produced `sel#bgColor = 8a8a8a` against `bgColor = 8a8a8a` -- a highlight that is present in the resource and invisible on screen. Worse in dark mode: derive is flattened against the LIGHT parent, so the dark thumb came out 8a8a8a, the light mobile grey, rather than the theme's own 9a9a9a. The test therefore asserts the two values DIFFER; asserting the key exists passes on exactly the defect being fixed. **Nothing defined the surfaces CN1 still draws on a desktop.** On Windows and macOS the menu BAR is the platform's own, but the right-click menu, the overflow menu, the tooltip and the dialog's command area are ours -- and the whole of GNOME's headerbar mode is. PopupContentPane, CommandList, Command, TouchCommand, Tooltip, TooltipDialog, DialogCommandArea, DialogButton and DialogButtonDefault were undefined in all three, so each fell through to UIManager's blank default: black text on white, on a dark window. Same for ToolbarSearch (written by SearchBar), the Accordion pair (seeded by the framework with a line border and phone metrics) and the Tabs trio (only the tabs* constants existed). Plus the three new components' UIIDs: Separator, GroupBox/GroupBoxTitle, Stepper/StepperField/StepperButton, and Link. **interactiveScrollBool and defaultNativeWindowModeBool are now theme constants.** Both are behaviours a native desktop theme IS, and the theme is the right seam for them: these three files install only on the desktop, so an application still on the legacy theme is untouched and no port-side isDesktop() gate is needed. Dialogs therefore open as real operating system windows by default under these themes; anchored popups never do, and the constant is ignored where there is no windowing system. Aqua adds no hover rules, which is asserted rather than remembered: AppKit restyles none of these controls on rollover, the captured reference says so, and eighteen .hover rules were already removed from that theme for the same reason. Its scrollbar knob is the deliberate exception, because NSScroller does darken -- through sel#/press#, like every other interactive thumb. Every coloured addition has a $Dark counterpart, and that is asserted too: the derive flattening above is exactly how a dark-mode rule goes missing without anything noticing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ned one Component.addContextMenuListener has been here for a long time -- it fires on a secondary mouse button, a stylus barrel button or a long press -- and nothing ever turned it into a menu. Every application that wanted one built its own popup, which is why none of them looked like the platform. ContextMenu is the popup, styled through PopupContentPane, CommandList and Command, which the desktop themes now define. Component.setContextMenuCommands is the short way in: give a component its commands and the menu opens by itself. When the commands depend on what was clicked -- which row, which cell -- a listener plus ContextMenu.show is still the way. Three details that are decisions rather than mechanics: The listener and the commands are resolved in ONE walk up the tree, not two. A row inside a table that carries its own commands must not be overruled by the table's listener merely declining to consume, and two passes would do exactly that. The buttons carry the command's NAME rather than the command itself. Button(Command) fires the command from inside its own action event, which runs it while the menu is still showing -- so a command that opens a form or another dialog would put it underneath a menu still holding the popup layer, and the menu would outlive the screen it belonged to. Here the menu is disposed first and the command dispatched after, which is the order Dialog already uses for its own command buttons. It never becomes an operating system window, even now that the desktop themes ask for that by default: setNativeWindowMode(false) per instance outranks both the static default and the theme constant. The reasons are the ones already written on Dialog.showPopupDialogImpl -- the rectangle it points at is in its host's coordinate space, a separate window would never receive the click meant to dismiss it, and it would steal focus from its opener every time it appeared. Empty means nothing opens, in both directions: setting null or an empty array removes the menu rather than leaving an empty one, and ContextMenu.show refuses to open one. A menu with no items is a rectangle the user has to dismiss to learn it was empty. resolveContextMenuOwner is the routing without the showing. The showing is a modal popup that parks its caller until the user dismisses it, so a test that asserted the routing through it would simply hang -- which is what the first version of these tests did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MenuBar.updateCommands hands the commands to setNativeCommands and RETURNS -- it draws no soft buttons, because on a platform with a real menu bar drawing them too would duplicate every command. CodenameOneImplementation.setNativeCommands is an empty method. So on a platform without a menu bar, asking for COMMAND_BEHAVIOR_NATIVE sent every command to a method that discards them and drew nothing: the application simply had no commands, and nothing anywhere in that path could tell that from "the platform handled it". Latent for as long as the constant has existed, because nothing asked for it. The desktop native themes now do -- `commandBehavior: Native` is right for the three platforms they model -- and two of the ports that will install them, Windows and Linux, implement no native menu bar at all: no setNativeCommands, no WindowManager.setCommands, no menu native source. Installing those themes without this would have deleted the commands from every Windows and Linux desktop application, silently. isNativeCommandsSupported() is the missing question, and setCommandBehavior is where it is asked. That method already normalises a behaviour the platform cannot honour -- BUTTON_BAR becomes SOFTKEY on a non-touch device -- so NATIVE becomes DEFAULT on a platform with no menu bar in the same place, and every reader downstream is fixed at once rather than one at a time. JavaSE and the iOS/macOS ports answer true on the desktop, where they really do build a JMenuBar and an NSMenu; everything else keeps drawing what it has always drawn. Form.isDesktopHideToolbar() needs the same question asked separately, because it is driven by desktopTitleBarMode rather than by commandBehavior. Hiding the Toolbar takes away the side menu, which is the only place the commands are drawn, so a port that cannot take them natively keeps its Toolbar. The title still goes to the OS title bar there, because that part works everywhere -- the result is the legacy look, not a broken one. Asserted in both directions, and the guards verified to bite by removing each one and watching the matching test fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three desktop native themes have been built, measured and gating since they landed, and no port installed one. Windows and Linux staged AndroidMaterialTheme -- a phone design language on a desktop, complete with Material ripples, a hamburger side menu and a fading touch scrollbar -- and macOS defaulted to the modern iOS theme, which is the same mistake with a different phone. That was sequencing, not oversight: the flip restyles every screen and reseeds each port's committed screenshot baselines. It was deferred until those could be reseeded alongside it, which is what this change does. Three one-line flips, exactly where the deferral notes said they were: - maven/windows/pom.xml stages WindowsFluentTheme.res - maven/linux/pom.xml stages GnomeAdwaitaTheme.res - MacOSBuildHints.getThemeMode()'s unset branch answers "aqua" The JavaSE desktop default is deliberately NOT flipped. resolveDesktopNativeTheme keeps answering legacy when nothing asks, because that default reaches every desktop application ever built with Codename One, not only ours -- and an application that wants the platform look already has desktop.themeMode and the cross-platform nativeTheme=native to ask with. The three native ports have no such history: none has shipped. hellocodenameone gains desktop.titleBar=native and desktop.interactiveScrollbars=true, which is what the Maven archetype and the initializr already put in every new project. The app's own theme.css sets includeNativeBool: true, so it inherits whichever theme the port stages with no CSS change. DesktopModeScreenshotTest used to switch desktop mode on for itself and switch it back off in done(), because it was the only screen in the suite that ran with the desktop chrome. It is not any more, and keeping the local opt-in would now HIDE a regression rather than demonstrate a feature: whatever the test turned on for itself would look right even if the suite-wide settings had stopped working. Its baseline is also the record of a real difference between the ports. macOS and the Java SE build have a menu bar, so the Toolbar is hidden and the commands move into it, out of the raster. Windows and Linux have none yet, so the Toolbar stays and draws them -- Form. isDesktopHideToolbar() will not hide the only place the commands exist. Both are correct, and the two baselines are what say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hanged COVERAGE.md's Desktop section has been a verbatim copy of the iOS section since 3959d68. That commit set out to remove a block the merge had duplicated and removed the wrong copy: what sits under "Desktop: Windows Fluent, macOS Aqua, GNOME Adwaita" today is UIButton .glass, UITabBar and UIPickerView. So the file has had no record at all of which desktop widgets are covered, which is the one thing that section exists to say. Restored from 2ceb4f0, the last commit that had it, and updated. What the update records: - Which theme each port installs now, and that the Java SE default is deliberately still legacy -- that one default reaches every desktop application ever built rather than only ours. - The constants the desktop themes declare, and why they are theme constants rather than port hooks: the three files install only on a desktop, so nothing needs an isDesktop() gate. - That commandBehavior: Native is safe on a port with no menu bar because setCommandBehavior normalises it away, which it did not before. - The native menu bar on Windows and Linux, added to the honest gap list rather than left to be discovered. README.md's Layout block listed ios-modern and android-material only, and its Rebuilding section the two themes those produce -- the three desktop ones have been missing from both since they landed. Added, along with a section on the behaviour a desktop theme declares, including the two traps this change had to find by measuring: the interactive scrollbar thumb highlights through .selected and .pressed rather than .hover, and cn1-derive on those UIIDs produces a highlight identical to the resting colour. The guide gains the keyboard conventions, the context menu, the three new components, and the note that Windows and Linux keep their Toolbar because they have nowhere native to put the commands -- which is a difference a developer will see and should not have to rediscover. Vale and LanguageTool both clean on the two chapters, checked against the committed versions first to be sure the findings were mine rather than inherited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The desktop matrix scored nine controls, all of them form fields. Everything that makes a window look like a desktop window -- the scrollbar, the menus, the chrome -- was unmeasured, including the interactive scrollbar that is the headline feature of this whole area. Thirteen rows added: ScrollBar, Separator, GroupBox, Stepper, LinkButton, SearchField, ListRow, Tabs, Toolbar, Disclosure, MenuBar, MenuItem, Tooltip. All at the existing 240x56 tile, because the tile size is a constant in each of the three standalone capture apps and nothing here needed a bigger canvas. A Dialog row does, which is why there isn't one; it is in the gap table with the prerequisite named. The CN1 scrollbar tile is the bar and nothing else. `LookAndFeel.drawVerticalScroll` paints the theme's track and thumb across any component, so the tile is the same thing the reference apps build -- a scrolling container would put its content in the comparison too. Its hover and drag states come from OVERRIDING `isVScrollThumbHover()` and `isVScrollThumbGrabbed()`, which are the two public methods `drawScroll` asks to choose between the unselected, selected and pressed thumb styles. That renders exactly the pixels a real hover produces, with no test-only hook added to the framework and no synthetic pointer to get wrong. Three rows are not scored on every platform, and that is the honest answer rather than a gap. A reference has to be renderable into a view: - macOS cannot capture a scrollbar. Measured, not assumed: an NSScroller reports usableParts=allScrollerParts, knobProportion 0.4, isHidden=false and a 17x56 frame, and renders NOTHING through NSView.cacheDisplay -- detached and inside a real NSScrollView, in both .legacy and .overlay styles, with AppleShowScrollBars=Always already set by the capture script. The tile comes back holding one colour, the backdrop, every time. Same class of limitation as Aqua vibrancy, and cacheDisplay is the path that needs no Screen Recording consent, which a hosted runner cannot grant at all. - NSMenu belongs to the window server, so the menu rows are Windows and GNOME. - The AppKit and GTK tooltips are separate windows; a WinUI ToolTip is an ordinary Control, which is the only reason that row exists. Two rows lost a state rather than compare different things. A native expander's expanded state reveals its content, so "expanded native" against "selected-styled CN1 header" is not one measurement -- DesktopDisclosure scores its resting header. And a menu item's highlight is hover on both platforms that have one, so DesktopMenuItem uses hover like every other row rather than a selected style nothing drives. The macOS reference is verified end to end here: 92 tiles, zero blockers, every new tile reviewed by eye. That review is what found three defects the app's own blockers had already flagged -- an NSTableRowView with no intrinsic size in either axis laying out to 240x0, an NSStackView whose fittingSize had no width so the stepper showed its chevrons and no field, and a CGColor read from a dynamic NSColor freezing at the wrong appearance so the light toolbar tile was painted with the dark window background. check-fidelity-spec.py's label table gains all eight new text-bearing rows, which is the check that caught all six of the first wave rendering different strings on the two sides. Its scraper needed two fixes to do it: follow a `MakeXxx()` factory arm the way it already followed MakeComboBox, and take the first NON-EMPTY literal -- the macOS disclosure builds an empty-titled button for the triangle before the label that says "Details", and match one read the empty string. Verified to bite by drifting the GNOME group box label. No goldens yet, deliberately: they come from the runner that scores them, never from a developer's Mac, which has a chosen accent colour, a chosen appearance and custom fonts. The desktop legs will report the new pairs as missing_expected and fail until fidelity-desktop-native-ref.yml is dispatched and its output reviewed and committed -- a new row failing is correct, and is not the same as being skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither port had one: no setNativeCommands, no getDesktopTitleBarMode, no menu native source. So the desktop themes' `commandBehavior: Native` and `desktopTitleBarMode: native` both had to be normalised away there, and the commands stayed in the Codename One Toolbar. Correct, and not what a desktop application looks like. Windows gets a Win32 HMENU hung on the application window with SetMenu; Linux gets a GtkMenuBar packed above the drawing area. Both take the row format IOSImplementation.setNativeCommands already writes for the macOS menu -- "<menuHint>\t<label>\t<shortcutKeyChar>\t<shortcutModifiers>\t<commandId>" -- so the three ParparVM desktop ports share one encoding rather than each inventing its own alongside Command's placement constants. Decisions worth recording, because none of them is forced by the API: **Where About, Preferences and Quit go.** Neither platform has an application menu, so the macOS mapping does not transfer: About goes under Help and the other two under File, which is where a Windows or GNOME user looks. That is why neither hint table is a copy of the macOS one. **Win32 menu ids are not command ids.** They are 16 bit and share a space with control notifications, so the ids handed to Win32 come from a private base and are mapped back. A Codename One command id is a 32-bit counter that would start colliding with WM_COMMAND's control notifications the moment it passed 0xFFFF. The high word is also checked before the range, so a notification whose control id happens to land in the range is not mistaken for a menu item. **Accelerators are drawn by hand on Windows and by GTK on Linux.** Win32 draws nothing itself -- the text after a tab IS the accelerator display -- so a shortcut not spelled into the label would respond and show nowhere. GTK's accel group draws it and binds it in one call. **Both rebuilds are BLOCKING hand-offs to the UI thread**, and that is load bearing twice over. SetMenu and GTK are not legal from the EDT, so a marshal is required either way -- but stringToUTF8 returns this thread's scratch buffer, which the next conversion on this thread overwrites, so a posted message would read it after it had moved on. A blocking send cannot: nothing else runs on this thread until it returns. **The Linux window gained a GtkBox between the window and the overlay**, and it stays EMPTY until commands arrive. A box with one child that expands lays the overlay out exactly as it was laid out as the window's direct child, so an application that publishes no commands renders identically to before -- which is what keeps the screenshot baselines of every such app untouched. **A selection goes back through the ordinary event queue** as CN1_EVENT_MENU_COMMAND, on the same number in both ports so the two desktop wire protocols do not drift apart. That is what puts the command on the EDT rather than on the pump or GTK thread. Each port keeps one superseded generation of its command map, because a selection can be drained after the form that published it has been replaced -- without it, the command a user clicked on the way out of a screen resolves to nothing and silently does not run. The C and C++ here have never been compiled: neither toolchain exists on the machine this was written on, so CI is the first verifier. What could be checked was: scripts/check-native-signatures.sh resolves all 483 Windows and 481 Linux natives including the two new ones, and both natives sit INSIDE their file's extern "C" block -- the exact mistake this PR series already shipped once in cn1_windows_window.cpp, where the name was right, the verifier passed, and only a real Windows build reported the undefined symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
The first capture run answered two questions this could only guess at, and both answers narrow a row rather than widen it. **A WinUI ScrollBar has no highlight state a capture can ask for.** None of PointerOver, UncheckedPointerOver, CheckedPointerOver or MouseOver is a visual state of that control, and neither is Pressed or Dragging -- the capture app's blocker said so by name rather than writing six tiles identical to normal. GTK can state it: PRELIGHT and ACTIVE are what the CSS pseudo-classes resolve from, and the captured GNOME hover and pressed tiles are genuinely different from their normal one. So the resting bar is one row scored on Windows and GNOME, and the highlight is a second row scored on GNOME alone -- which is the only place all three of "the reference renders", "the reference can be put in the state" and "the state is visible" are true at once. **A WinUI ListViewItem draws its own pointer-over chrome.** It goes through ListViewItemPresenter, which paints rather than exposing a visual state, so hover is dropped from the row. Selected is a real property on all three and is still scored. Neither is a theme problem and neither is fixable by tuning, which is why both are recorded in the gap table with what was measured rather than left to be rediscovered by the next person who wonders why the scrollbar highlight is not scored on Windows. macOS and GNOME captured cleanly on that run -- 92 and 104 tiles, no blockers -- so this is the only change the run asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither is reachable on a healthy run, which is exactly why they would have sat there. **Linux created a fresh GtkAccelGroup on every rebuild** and added it to the window. The menu items go away with the bar, but the groups do not: a window would end up holding one group per form that had ever published commands. Created once and reused. **Windows leaked a popup whose title failed to widen.** The popup was created before the label conversion and only appended to the bar afterwards, so a failed conversion left it allocated and unreachable -- DestroyMenu on the bar cannot reach a menu that was never appended to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
Both found by scoring the CN1 side against the first CI capture and then looking at the tiles, which is the step the golden protocol asks for and the only one that catches this class of problem: the numbers were plausible, and one of them was measuring a control cut in half. **NSTabView was clipped through its own tab strip.** Left to its fitting size it is taller than the tile, so the pill came out sliced along its top edge. Full height now, like the group box. A reference that is cut in half measures nothing, whatever the number underneath says. **A selected NSTableRowView drew the unemphasized grey.** Outside a focused table that is the default, and grey is what macOS shows for a selection in a window the user is NOT working in -- not what a selected row looks like while they are. The CN1 row is correctly accent-filled, so the comparison scored a correct style against a reference in the wrong state: 67%. isEmphasized makes it the accent fill in both appearances. Neither is a theme finding and neither would have been visible from the score alone. Also corrects DesktopModeScreenshotTest's note, which said Windows and Linux have no native menu bar. They do now, so the commands leave the raster on every desktop port and the Toolbar-stays branch is asserted by DesktopChromeTest rather than by a screenshot no port produces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A WinUI TabView is a document-tab control and puts a close affordance on every tab. A Codename One Tabs has no such thing, so the tile compared two tabs against two tabs plus two buttons and charged the difference to the theme. IsClosable false on both items. Found by looking at the captured tiles, which is the review step the golden protocol asks for -- the score alone would have read as "the Fluent tab styling needs work". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 new tiles across the three desktop sets, captured by fidelity-desktop-native-ref.yml run 35370740065 on the hosted runners -- never locally, so no developer's accent colour, appearance or installed fonts are baked in. Every new tile was reviewed by eye before this commit, which is the step that found the four reference bugs fixed in the commits above it. Set sizes now match their manifests exactly: macOS 88, GNOME 104, Windows 100. macOS and GNOME carry fewer rows than Windows, and that is the recorded per-platform scoping, not a short capture -- NSMenu and both tooltips are window-server surfaces, and AppKit cannot render a scrollbar into a view at all. Reproducibility, checked by comparing this run against the previous one: - GNOME reproduced BYTE-FOR-BYTE across two independent runs, all 104 tiles including the manifest. So nondeterminism is not a property of the suite. - macOS differed on exactly the four tiles the reference fixes targeted and nowhere else, 84 of 88 identical. - Windows differed on three tiles beyond the intended one, and all three are the documented residual: 2-3 pixels, +/-1 in a channel, on an anti-aliased edge. The README said that residual was the slider thumb; it is now measured on the tooltip's rounded border too, so it is recorded as a property of anti-aliased edges on that runner rather than of one control. One file is deliberately NOT in the Windows set. The capture also writes Button_normal_light.png, the BitBlt self-check that proves the Mica backdrop reached the window; it is excluded from tiles_written because it is not a tile, has no CN1 counterpart, and would make the golden count disagree with the number of pairs the gate can score. No baselines here. Those are recorded separately, from the runs that SCORE these, so the commit that defines the goldens and the commit that defines the ratchet stay two reviewable changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exit 139, partway through the dark pass, several widgets after the menu bar tile had been written -- and the two runs before it completed all 104 tiles from the same binary. That shape is a deferred free: the damage is done at teardown of one widget and surfaces later. The menu bar was the only new widget handing a GObject back to GTK and then dropping its own reference. GtkPopoverMenuBar keeps the model it was built from and rebuilds its items from it while it lives, and the popovers it creates hold the submenu, so unreffing both made the lifetime depend on GTK's teardown order rather than on ours. The models are now held for the life of the process: two objects in a tool that writes a hundred PNGs and exits, against a capture that fails one run in three. That is a diagnosis from the crash's shape rather than from a backtrace, because there was no backtrace to read -- which is the second half of this change. A fatal-signal handler now prints the frames and re-raises, so the shell still sees the real signal and the job still fails, and the next occurrence says where instead of only that. backtrace_symbols_fd rather than backtrace_symbols: it writes straight to the fd and allocates nothing, which is what makes it safe from a signal handler. The build gains -g and -rdynamic, without which the frames print as bare addresses. If it recurs the handler will say so plainly; it will not be quietly retried. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
MacOSStubThemeTest required the generated stub to call setIosMode("modern"), and the theme
flip broke it -- correctly, since the default is aqua now. But the literal was never what
the test was guarding. Its own comment says so: the point was that iOS7Theme.res declares no
@darkModeBool, so a stub defaulting to it leaves an application with no dark mode however
carefully it asks, and every dark screenshot comes out light. "modern" was simply the theme
that happened to satisfy that.
So it is now two tests. One pins the value, because the value is a decision worth pinning.
The other names the property -- the default is never the theme with no dark mode -- so the
next person to move this default is told what it has to keep rather than reading a literal
and guessing. Aqua declares the constant too, which DesktopNativeThemeContentTest asserts
against the compiled resource rather than the CSS.
The flip also made a latent hazard load bearing, so that gets a test as well.
installNativeTheme() asks for its resource by name at run time and falls back to the legacy
theme WITHOUT SAYING SO when the lookup returns null -- the file's own comment warns about
it. While the default was modern, iOSModernTheme.res was the only theme anyone staged, so
nothing could go wrong; with the default on aqua, a MacOSAquaTheme.res that fails to reach
buildinRes is a silent revert to the iOS 7 look on every macOS build. stageThemeResources
copies every .res it finds, so the flip is safe today, and now something says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 157 screenshots: 157 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
The desktop themes set defaultNativeWindowModeBool, and the Windows port's screenshot suite
then captured two of LightweightPickerButtons' four placements, found the two it did capture
byte-identical, and timed out waiting for the rest.
The lightweight Picker positions its popup by hand -- setX/setY/setWidth/setHeight, then
show(top, bottom, left, right) -- and native window mode documents exactly those margins as
ignored. So in a window the popup comes out centred and every placement variant collapses
onto the same picture.
This is a bug in the native-window-dialog feature rather than in the theme default: an
application calling Dialog.setDefaultNativeWindowMode(true) hit it just as hard. ComboBox and
InfiniteProgress had already opted out by hand, so the hazard was known -- it had simply
never been reachable, because nothing defaulted the mode to true. Six more sites had not:
TooltipManager, both Toolbar side menus, both Picker popups, Validator and
FloatingActionButton.
There is no choke point to fix it at. Dialog.usesNativeWindow() already exempts menus and
anchored popups, but AbstractDialog is an interface whose own comment forbids new members --
the core is Java 5, so a new method breaks every implementation and throws AbstractMethodError
on the compiled ones. So each site says so itself, and a test enumerates them.
That test reads SOURCE, and the first version of it did not:
The obvious runtime test walks the current form looking for a popup that wants a window.
It passed with every fix removed. Reaching one of these popups means pressing the control
that owns it, which parks the caller, so the popup does not exist while the test can look.
A probe that cannot fail proves nothing.
What it counts is constructions against opt-outs, per file, because the bug that started
this was the SECOND of a pair being missed -- and it immediately found a third: Validator
builds two, and the one I had fixed was not the field initialiser that is used until a
constraint supplies its own message.
It also strips comments first, because BubbleTransition shows a dialog in a javadoc EXAMPLE
and counting that reported a file with no dialogs in it as an unclassified builder. A check
whose findings have to be filtered by hand stops being read.
A second test fails when a new framework source starts building one of these and is in
neither list, so the classification stays a decision rather than a default. Seven existing
sources were classified by it: the file choosers, crash reporter, signature pad, share sheet
and country list are dialogs the USER operates and correctly take the default, and MenuBar is
exempt by mechanism because usesNativeWindow() already refuses a window for a menu.
Also lands the fidelity baselines from run 35371376318 -- 86 macOS, 102 GNOME, 98 Windows
pairs, all with geometry, means 84.0 / 83.8 / 85.3 against 84.6 / 85.9 / 82.1 for the
nine-row matrix. And drops DesktopSeparator, because the gate refused to baseline it and was
right to: a 1px hairline a few levels off the surface does not clear the comparator's content
threshold, so the tile is 98.2% backdrop holding exactly TWO colours, geometry comes back
{"empty": true}, and shape and size agreement reach 1.0 because two empty masks agree
perfectly. Windows light scored 96.45% on a comparison that had looked at nothing. The
component is still themed and still asserted; it is the overlay comparison that cannot see it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 223 screenshots: 223 matched. |
…open Found by looking at a captured screenshot rather than at a score. TabsTheme_light on the Linux port came back as three plain boxes with hard borders and no indication whatever of which tab was open -- and the DesktopTabs fidelity row had scored 35-68%, the worst in the set, which I had written down as "CN1's tabs do not look like a native tab control". That diagnosis was wrong. Tabs writes the `Tab` UIID onto every tab button and marks the open one with that button's own SELECTED style. These themes styled `SelectedTab` and `UnselectedTab`, which nothing in the framework writes at all -- I added them on the assumption that the names meant what they say. Dead rules. With nothing styling `Tab`, the strip fell through to UIManager.resetThemeProps, which seeds `Tab.sel#derive: Tab`. That seed makes the selected tab derive from the unselected one, so the two are pixel-identical by construction. A tab strip that cannot show which tab is open is a usability defect, not a fidelity gap, and no amount of tuning the score would have found it. `Tab` is now styled per platform, with the selected treatment each one actually uses: a card fill on Fluent, the accent pill on Aqua, an accent rule under the label on Adwaita. Plus `TabbedPane`, `TabsContainer` and `TabsContainerHost`, which `Tabs` also names and which no desktop theme defined either. The test asserts the DIFFERENCE, not the key, and for the same reason the scrollbar highlight does: `Tab.sel#` is present in every compiled theme whether or not anything styled it, because the framework seeds it. Present-and-equal IS the defect. Verified to bite by setting the selected rule back to the base colours and watching it fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 150 screenshots: 150 matched. |
Its javadoc says the golden "shows the dialog centred on the window with the window's content dimmed behind it, which is only possible if both are on the same surface" -- that is the HOSTED path: the window's layered pane, the scrim, isTopmostHostedDialog, a real amount of code. The desktop themes set defaultNativeWindowModeBool, so the dialog now opens as a separate operating system window, leaves the captured raster entirely, and the golden becomes an empty host window. Correct behaviour for that mode, and no longer a test of anything. Found by scanning the reseed captures for screens that had gone uniform rather than by reading a diff: Window-Dialog-900x700 came back 99.7% a single colour, and the control run over the committed baselines showed it had not been near the top of that list before. The other Window-* screens were already sparse for their own reasons, which is why the control mattered -- high uniformity is normal in this suite and only the CHANGE is a signal. Pinned to the hosted path, so it keeps covering the code it was written for. An application can still ask for that mode, so this is a supported configuration and not a workaround. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scoring the macOS leg after the Tab styling landed separates two things the single 35-68% number had been hiding. The DEFECT is fixed: the selected tab is now visibly selected, and the light row moved 68 -> 82. Confirmed by looking at the tiles rather than by the number -- CN1 now draws a filled "One" against a plain "Two". What remains is SHAPE, and it is not a tuning problem. CN1 draws a left-aligned row of tabs; AppKit draws a centred rounded pill inside a grey track. The dark row is still 49%, and the geometry moved FURTHER from native -- height ratio 0.53 -> 0.25 -- because the styled tab is shorter than the unstyled full-height box it replaced. Recorded as wanting a per-platform tab shape rather than more colour tuning, so the next person does not read the improved light score as the row being done. The run also reported seven sub-1% drops on TextField and GroupBox and a handful of AccentButton width drifts. Those are NOT acted on: the baseline was recorded on the CI runner and this scoring was local, which is the disagreement goldens/README.md already documents in the other direction -- a Mac-recorded baseline failed the gnome gate on eighteen pairs over a slider one pixel taller on Linux. A sub-1% delta between a local render and a runner-recorded baseline is that, not a regression. The baseline re-record itself therefore waits for the dispatched CI scoring run, from the runner that scores it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI scoring run confirmed the Tab fix is real and large -- GNOME dark 35.73% -> 73.09%, light 64.67% -> 75.75%, with no score regressions anywhere. It also showed the geometry moving the wrong way: width ratio 1.0 -> 0.64 and the centre offset 12.5px -> 43.6px. That is a consequence of the fix rather than a separate bug. A Tab is transparent until it is selected, so once the tabs were styled the only content in the tile was the selected tab's fill and the two labels -- the comparator's bounding box shrank to the part that paints. GtkNotebook and a WinUI TabView both draw a divider under the tab strip, and neither theme had one. Adding it fixes the look and the measurement together, which is the only kind of change worth making to a geometry number: drawing the line the platform actually draws, not padding something out until a ratio improves. Aqua deliberately gets no such rule. NSTabView's pill sits on the bare window background, and for that theme a narrower bounding box is CORRECT -- its native reference is a centred pill, not a full-width strip, so the ratio moving away from 1.0 is the CN1 side getting closer to the reference rather than further. Worth recording from the same run: the seven sub-1% "regressions" my LOCAL scoring reported on TextField and GroupBox do not appear on the runner at all. That is the local-versus-runner disagreement goldens/README.md documents, measured from the other side this time, and it is why the baseline re-record has to come from the runner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…emes The capture that confirmed the fix also falsified the claim, so the claim goes rather than staying as a plausible-sounding comment beside working code. DividerStrokeColorDefault is translucent black in light and translucent WHITE in dark: the corrected rule came out #323232 on #202020, contrast 18 against 2 before, which is the band the Aqua (21) and Adwaita (29) references sit in. CardStrokeColorDefault is translucent black in BOTH themes at different alphas, so the corrected GroupBox frame came out #1D1D1D -- the right resource, and still nearly invisible. 0x20 * (1 - 25/255) is 0x1D and the light brush lands on 0x1E, which is how the capture distinguishes "now resolving the dark brush" from "still resolving the light one" at a distance of one. That faintness is Fluent, not a bug: a card stroke outlines a CardBackgroundFillColorDefaultBrush fill lighter than the page, and it is the fill the stroke reads against. This reference draws the stroke with no fill. Giving it one would be faithful and would also change what the CN1 GroupBox UIID has to match, so it is recorded rather than done here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Captured by fidelity-desktop-native-ref run 35498153403 on the arm64 windows-latest runner: Windows 11 25H2 build 26200, WinUI 3 unpackaged self-contained on .NET 9.0.20, Segoe UI Variable present, backdrops #F3F3F3 and #202020, no blockers. Only these two tiles are promoted, because only these two changed for a reason. DesktopSeparator_normal_dark is the whole 240x1 rule at a delta of 20 (#1E1E1E to #323232); DesktopGroupBox_normal_dark is its 320-pixel frame at a delta of 1 (#1E1E1E to #1D1D1D, the dark card stroke instead of the light one). The run also differed from the committed set on DesktopSlider_normal_dark, DesktopSlider_hover_dark and DesktopTooltip_normal_light, by +/-1 in one channel on one or two anti-aliased edge pixels each. That is the GPU rasterizer residual goldens/README.md already records and re-records; it is far below the comparator's content threshold and rewriting those three would churn bytes to say nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two binary conflicts, both hellocodenameone macOS goldens, and both sides had changed them for a real reason: this branch reseeded the whole macOS set for the Aqua theme flip (c957f8c, run 35395172908), and master's #5870 changed the dialog title from bold to regular weight while fixing a queued animation that lost its updateState(). So NEITHER side's bytes are right for the merged tree, which has the Aqua theme AND that fix. Resolved to ours because the theme flip rewrites 75-78% of each image while master's delta is 0.3% in the title, so taking master's would leave two tiles inconsistent with the other ~158 in the set. These two therefore still encode the pre-#5870 title weight and have to be re-captured from a macOS run on this merged head. That is not left to memory: scripts-macos.yml compares the runner's render against these goldens, so if the title weight does change under Aqua the job fails and names them. Verified across the merge rather than assumed: no conflict markers, no file deleted relative to the branch tip, this branch's Form.java desktop work (escapeConsumedOnPress, desktopKeyReleased, isReachableForTraversal, DesktopTabIteratorFilter) intact beside master's layered-pane paint change, and port_status.json lost no id and no screenshot mapping from either side while gaining master's foldable-posture entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t both It failed the whole macOS run on NSSlider, whose tiles have been correct for as long as the set has existed. Any-alpha-below-255 was the wrong test: the tile is opaque by construction, so a translucent pixel does mean a control replaced what was under it rather than blending onto it, but a control that is simply translucent does that harmlessly. NSSlider's knob is 276 pixels of one uniform fill. What is fatal is an alpha channel carrying IMAGE STRUCTURE -- a control drawn as a pure coverage mask, constant RGB with its shape and its glyphs in alpha, which read back as RGB is a solid block of the mask colour with its own label invisible against it. The two are separated by counting distinct alphas among the pure black/white translucent pixels, and the separation is measured, not judged: over the committed set every NSSlider tile has exactly ONE, and the two broken NSTabView tiles have 90 and 126. The threshold is 8, an order of magnitude from both. Replayed over the committed goldens in both premultiplied and un-premultiplied form, all six slider tiles pass and both tab tiles block. Second bug, found by probing instead of assuming: the rep is PREMULTIPLIED by default, so a pure white pixel at alpha 13 is stored (13,13,13) and testing every channel for 255 found the Aqua mask (black, stored (0,0,0) either way) and silently missed the Dark Aqua one. The probe reported the light tile only; both now fire, 97 alphas each. The PNG on disk is un-premultiplied, which is why the committed golden reads (255,255,255,13) and the buffer it came from does not. Also corrects a claim in the previous commit's comment: promoting does NOT flatten the alpha away. The goldens are RGBA and keep it. What reads the tile as RGB -- a viewer, and the comparator that scores it -- is what turns the masked region into solid black or white. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Captured by fidelity-desktop-native-ref run 35500161522 on macos-15 (15.7.9,
build 24G830): backing scale 1.0, default accent #007AFF, backdrops #E7E7E7 and
#262626, no blockers, 88 tiles, and the refined coverage-mask check passed the
whole set -- including the six NSSlider tiles whose uniformly translucent knob
failed its first version.
Exactly these six differ from the committed set and nothing else does:
DesktopTabs_normal_{light,dark} the segmented reference replacing NSTabView.
Light was "One" beside a solid black block;
dark was two blank white pills. Both now
render both labels.
DesktopListRow_{normal,selected}_* "Row" moved from x=7 to x=17, off the
selection capsule's edge where its R was
clipped against the fill, to the 7pt inset
AppKit itself uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From linux-build-run run 35498811062, the first run on the merge commit, both arches. Master fixed a queued animation that lost its updateState() and, in the same change, stopped FormLayeredPane drawing the form beneath it during the form's own paint pass -- which had been painting the whole tree a second time into the same frame. Opaque fills survive that unchanged. ANTI-ALIASED TEXT DOES NOT, and that is the whole of this diff: identical layout, identical glyph positions, 1444 and 1751 pixels differing by at most 36, all of them text edges plus one border row, with the capture LIGHTER than the golden in every case (Sheet 138.6 against 127.4, AppReviewDialog 142.9 against 132.8, byte-identical numbers on x64 and arm64). Heavier text is the double paint; the capture is the single one. So these four goldens encoded the bug. Master reseeded exactly the same two tests for android, mac-catalyst and macos in that commit; its Linux and Windows sets did not move, and this branch's Linux set does because it renders under Adwaita. Windows cross-build passed unchanged on the same merge commit. Not a tolerance question and no sidecar was added: the old bytes are wrong, not noisy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-recorded from scripts-fidelity-desktop run 35501085353. Six pairs moved and
no other pair on any of the three platforms moved at all, which is what makes
the two runs comparable rather than merely sequential.
DesktopTabs_normal_dark 48.96 -> 78.60 the repair; it was the lowest
pair in the matrix, scored
against a flattened coverage mask
DesktopTabs_normal_light 81.97 -> 79.04 the same repair, downward: the CN1
render resembled a white pill beside
a black block slightly more than it
resembles a real segmented control
DesktopListRow_normal_* -5.88, -4.99 see below
DesktopListRow_selected_* -0.54, -0.34 centre offset improved 2.12 -> 1.50
The gate asked for the dark jump to be confirmed rather than accepted quietly,
which is the ratchet working: a leap that size is usually the measurement
moving, and here it was, in the direction of correct.
The ListRow drop is a REAL DIVERGENCE the corrected reference exposed, and it is
deliberately not fixed in this commit. Measured on the run's own tiles: AppKit
puts the row label at x=17 and CN1 puts it at x=9; AppKit insets the selection
capsule to x=10..229 while CN1 fills 0..239; AppKit insets the tab box to 7..232
while CN1 fills 0..239. So the theme's ListRenderer left padding is short and
neither it nor Tabs insets its background. Closing that means editing
native-themes/macos-aqua/theme.css, rebuilding MacOSAquaTheme.res and reseeding
every hellocodenameone macOS screenshot showing a list -- a change with its own
blast radius that does not belong in a commit fixing reference images. It is
recorded with its numbers in baseline/README.md so it stays findable instead of
disappearing into a lowered score.
DesktopSeparator is left unbaselined. The run reports it as a new pair at 91.68
and 90.84, which is the inflated number fidelity-tests.yaml already explains:
the content mask is empty on both sides, so shape and size agreement are 1.0 by
construction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From scripts-macos run 35502304966. These are the two files the merge with master conflicted on, and the resolution took OURS deliberately -- the Aqua flip had rewritten 75-78% of each image and master's delta was 0.3%, so taking master's would have left two tiles rendered in the old theme while the other ~158 were Aqua. The commit said they still encoded the pre-#5870 title weight and would have to be re-captured, and that the macOS job would name them rather than the point being left to memory. It named them. Same signature as the Linux pair, measured the same way: 940 and 1409 pixels differing by at most 42, all of them one line of text, with the capture LIGHTER than the golden (83.1 against 58.4, and 83.7 against 60.0). Side by side the golden's "Tap sheet to dismiss" and "Rating sheet" are visibly heavier. Which also settles what the merge commit could only describe: master's change did not restyle the dialog title from bold to regular. The title was never bold. It was anti-aliased text painted twice into one frame, and #5870 stopped the second pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two conflicts, and both resolved toward master because master deleted the thing this branch had been maintaining. #5875 removed the OpenGL ES renderer from the iOS port, taking the whole scripts/ios/screenshots lane with it -- 159 files. This branch had modified 12 of them and added 6 more (the DesktopChromeTheme / DesktopScrollbarTheme / DesktopWidgetsTheme pairs, seeded across all eleven sets). Git reported those as modify/delete; the deletion stands, because a golden for a renderer that no longer exists gates nothing. Checked rather than assumed: all six of those tests still have goldens in scripts/ios/screenshots-metal, which survives, and the tracked tree has no reference left to the deleted directory. #5873 split the iOS theme into common/gen26/gen27, which collided with this branch's addition of the three desktop directories to native-themes/README.md's Layout block. Both wanted: master's three iOS part files are in, re-padded to the column the desktop lines already use so the block stays one table. Verified across the merge: no conflict markers, this branch's Form.java desktop work intact (escapeConsumedOnPress, desktopKeyReleased, isReachableForTraversal, isDesktopHideToolbar), check-fidelity-spec still consistent at 47 components, and port_status.json lost no screenshot mapping from either side. The three ids it does lose -- "iOS (OpenGL)", "ios-gl" and "iOS (Metal)" -- are master's own doing: the GL port is gone and the Metal one is now just "iOS", id ios-metal unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…decided alone
Reported from a native build: the title area and the commands were still drawn
in the app. They were, and the build hint that was supposed to prevent it had
never worked on that port.
Form.getDesktopTitleBarMode() documents the precedence plainly -- "the build
hint wins: a project that spelled out desktop.titleBar means it, and a theme
must not talk it out of that" -- and the ports read the hint back through
Display.getProperty("desktop.titleBar"). Nothing ever put it there.
Display.getProperty falls through to the implementation, whose base answer is
the caller's default; LinuxImplementation and WindowsImplementation do not
override getProperty at all, and while IOSImplementation does, the property is
not among the eleven keys its builder emits. Its own comment said the stub
surfaced it. No builder did. So getConfiguredDesktopTitleBarMode() answered null
on every ParparVM desktop build and only the theme constant was ever consulted.
JavaSE was unaffected: it reads codename1.arg.desktop.titleBar as a system
property, which is why the simulator honoured a hint the devices ignored.
That is why this looked half-correct. Fluent and Aqua both declare
desktopTitleBarMode: native, so Windows and macOS hid the Toolbar for a reason
that had nothing to do with the project asking. Adwaita declares custom, because
GNOME's HeaderBar IS the title bar -- so on Linux, hellocodenameone's explicit
desktop.titleBar=native lost to the theme and the app kept a CN1 Toolbar with a
hamburger instead of moving its title to the window and its commands to the GTK
menu bar. Measured on the committed goldens: Windows and macOS DesktopMode start
at the top of the tile, Linux draws a title strip.
Executor.desktopTitleBarStubProperty emits the property, and the three builders
that produce a desktop target call it. Emitted ONLY when the project set the
hint -- a default would destroy the distinction getConfiguredDesktopTitleBarMode
exists to express, where null means "nobody asked" and lets the theme answer.
Normalised and validated to the three modes the wrapper mojo already validates,
because an unmatched mode reads as "toolbar", which looks exactly like the hint
being ignored again.
DesktopTitleBarStubPropertyTest pins it, and was checked against the old
behaviour rather than assumed to work: with the helper stubbed back to returning
"", three of its five tests fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported from scripts/macos/screenshots/TabsTheme_light.png: the selected tab is
a saturated blue block with white text and a big icon above it. That is a phone
tab bar. AppKit draws a raised PILL and leaves the label in the ordinary label
colour.
The theme said so literally. macos-aqua/theme.css had
Tab.selected { color: #ffffff; background-color: #007aff; ... }
in both the light rules and the dark block -- the iOS selection style it was
derived from, never revised. The other two desktop themes were written
correctly and are the control: Fluent uses a light pill (#fbfbfb with #1a1a1a
text) and Adwaita an underline (transparent with a #3584e4 bottom border), each
matching its platform. Aqua was the only one left carrying the phone look, which
is why its DesktopTabs pair sat at 79% while its neighbours' geometry agreed.
The replacements are measured off the captured AppKit reference rather than
chosen. From goldens/macos-aqua/DesktopTabs_normal_*: selected is #ffffff with
#262626 text in light and #6a6a6a with #ebebeb in dark; the unselected label is
#202020 / #e1e1e1. The label does NOT invert to white on selection on this
platform, which is the specific thing the old rule got wrong.
Also fixes light Tab.pressed, which was #ffffff text on an #e8e8e8 fill -- white
on near-white, unreadable whenever a tab was held.
Themes/MacOSAquaTheme.res rebuilt from it. The other themes recompiled
byte-identically in the same run, which is the check that nothing else moved.
The macOS Tabs screenshots and the DesktopTabs fidelity pair both change as a
result; goldens follow from the CI run, not from here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ecorded that Found while reviewing what the desktop.titleBar fix moved on Mac Catalyst. It is not a Catalyst problem: it has been true on Windows and macOS since the theme flip in this branch, and I reseeded those goldens without noticing. The suite sets desktop.titleBar=native. In that mode the Toolbar is deliberately never attached to the form -- the title goes to the OS window and the commands to the platform menu bar -- so ToolbarThemeScreenshotTest, whose entire subject is how a theme styles the Toolbar, captured its one body Label on an otherwise empty form. The committed goldens say so plainly: scripts/windows/screenshots/ToolbarTheme_light.png 9 colours, 99.8% one scripts/macos/screenshots/ToolbarTheme_light.png 237 colours, 99.7% one A screenshot of an empty screen cannot fail, so the gate was still green while the thing it guards had gone. Same shape as the "guard must not disable the mechanism under test" trap, arrived at from the other direction: the feature disabled the test rather than the test disabling the feature. Hiding the Toolbar is right for the application and stays. This test asks for toolbar mode for the duration of its own capture and puts the setting back after, so it renders the Toolbar it exists to photograph on every port. Restoring it needs a hook, and done() is not one: it is called speculatively -- the JS port force-dones after the light emit -- and DualAppearanceBaseTest's gate swallows those, so a subclass overriding done() cannot tell a premature call from the real one and would restore between the two appearance passes. finish() runs exactly once and already restores dark mode and the app theme for this same reason, so restoreAfterCapture() is called from there. Restoring writes back the PREVIOUS value including null, which Display.setProperty removes from localProperties. Null is not "toolbar": it means the project asked for nothing and the installed theme's own desktopTitleBarMode constant answers. Writing "toolbar" back would silence that for every test after this one. The Windows and macOS ToolbarTheme goldens have to be reseeded from a run with this in, since they currently record the empty screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file has always started at its package declaration. The header gate only checks files a change adds or modifies, so it went unnoticed until the previous commit touched it -- which is the gate working, on a file that predates it. Header copied verbatim from DualAppearanceBaseTest beside it, which is the Codename One GPLv2 + Classpath Exception form the script accepts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y form From ParparVM Java Tests (Windows) run 35542214753 on a05f6fc, the first run with the test's toolbar-mode override in it. The goldens being replaced are the ones the theme flip quietly produced: 9 colours, 99.8% a single colour, because desktop.titleBar=native meant the Toolbar was never attached and the test photographed its one body Label. The replacements show what the test exists to capture -- the hamburger, the "Theme Gallery" title, the search action and the overflow button -- in both appearances. Only these two tiles moved; the suite reports 192 pass, 1 fail on each arch. Seeded from the x64 capture and used for both arches, which is what the workflow asks for: x64 and arm64 compare against this one directory. They are byte-identical here, checked rather than assumed, so the shared golden is not papering over a per-arch difference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a Toolbar From scripts-macos run on a05f6fc. Five tiles across three tests, and nothing else in the 196-test suite moved. TabsTheme_{light,dark} and TabsAnimatedIndicatorScreenshotTest are the reported defect: the selected tab was a saturated #007aff block with a white label and a white icon, which is the iOS selection style the Aqua theme had never been revised away from. It is now the raised pill AppKit actually draws -- white with a black icon and label in light, grey in dark -- with the unselected tabs in the ordinary label colour rather than a phone tab bar's grey. ToolbarTheme_{light,dark} replaces goldens that were 237 colours and 99.7% a single colour, because desktop.titleBar=native had stopped the Toolbar being attached and the test was photographing its one body Label. They now show the hamburger, the title, the search action and the overflow button. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…enu bar From Test Mac Catalyst UI build scripts run 35542214958 on a05f6fc. 145 tiles, 62 unchanged. Catalyst is a macOS desktop target -- isDesktop() is isRunningOnMac(), and isNativeCommandsSupported() is documented as "the native macOS build's real NSMenu, and Catalyst's UIMenuBuilder" -- so once desktop.titleBar reached the port at all, this build started honouring the suite's desktop.titleBar=native like the macOS port already did. The Toolbar is no longer attached; the title is the window's and the commands are in the menu bar. Neither is in the raster, which is why so many tiles moved. Every one of the 145 was classified before being promoted, not counted: the golden's top band is gone and its content sits where the band was, measured as a 56-73px upward shift matching the golden at 86-100% on a sample, and the outliers are the same thing on screens that reflow rather than shift. The four near-blank captures were each opened: BrowserComponent is a black web view whose only other content was the title, landscape is "Testing orientation lock..." without its "Orientation Lock" title, and the two that worried me -- ToolbarTheme_light and ToolbarTheme_dark -- are NOT in this set at all. They dropped out when the previous commit gave that test back its Toolbar, which is the check that the fix works: a blank ToolbarTheme would otherwise have been promoted here to a third port. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the window's
Reported twice, from scripts/linux/screenshots-arm/graphics-draw-image-rect.png
and the Windows one beside it. Both still show a title bar; macOS and Catalyst
do not. Same suite, same test, same "native" title-bar mode.
Two code paths draw a form's title and only one of them had been taught about
desktop mode. A form WITH a Toolbar goes through Toolbar.initMenuBar, which
honours Form.isDesktopHideToolbar() and is never attached in native mode. A form
without one -- which is most of this suite, built by createForm(title) -- goes
through the legacy MenuBar/title-area path, and that asks
Display.isNativeTitle(). The iOS/macOS and JavaSE ports answer that
("isDesktop() && native"), so their title area disappears and the title is
pushed to the OS window. The Windows and Linux ports never overrode it and took
the base false, so they kept painting the strip while the mode said the title
belonged to the window. The ports disagreed with themselves on the same screen.
isNativeTitle() alone would only have LOST the title: neither port could set its
main window's title after startup. Both take it as a CreateWindowExW /
gtk_window_new argument from initDisplay, and desktopWindowSetTitle addresses
the secondary Window peers by slot. So each gets a mainWindowSetTitle native:
GTK's marshalled onto the main loop like every other widget call here, Win32's
straight through SetWindowTextW, which is documented as callable from any
thread. Both are no-ops with no window, which is what headless screenshot mode
needs.
refreshNativeTitle() skips Dialogs deliberately -- a Dialog paints inside the
form rather than owning the window, so letting one retitle the window would
leave its title behind after dismissal.
Verified, and the first attempt at verifying was worthless: check-native-signatures
passed against target/classes that predated these methods, so it never saw them.
After rebuilding both port modules it counts one more native each (485 and 483)
and resolves them. Removing a single underscore from the Linux C name then makes
it report "declared native but nothing implements it" with the required
signature, which is the check actually biting rather than passing over nothing.
check-native-cpp-linkage confirms the Win32 native sits inside the file's
extern "C" block.
Every Toolbar-less form in the Windows and Linux suites loses its title strip as
a result; those goldens follow from CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…agreed Reported a third time, and the previous commit did not fix it: isNativeTitle() was the right lever but it was reading a mode that never said "native". Form.getDesktopTitleBarMode() resolves configured -> INSTALLED THEME -> port. LinuxImplementation.getDesktopTitleBarMode() and the Windows one resolved configured -> "toolbar", with no theme step. So on the same screen Form read "native" from the Fluent theme and never attached the Toolbar, while the port read "toolbar", isNativeTitle() stayed false, and the LEGACY title area went on painting a title the mode said belonged to the window. That is why ToolbarTheme captured an empty form on Windows while graphics-draw-image-rect kept its strip -- one screen answered by each half of the split. JavaSE already had this and already fixed it; its resolveDesktopTitleBarMode carries the note, and there the split cost the application its commands. These two ports never got the same treatment. They do now. Second half: the Windows and Linux screenshot suites do not build through WindowsNativeBuilder or LinuxNativeBuilder at all. CleanTargetIntegrationTest and CleanTargetLinuxIntegrationTest hand-write their own launcher, so none of the generated stub's Display properties exist in those runs -- including the desktop.titleBar that hellocodenameone's own settings set and that Executor.desktopTitleBarStubProperty now emits for a real build. Both launchers therefore set it explicitly. Without that the suites keep rendering chrome no shipped application has, and their goldens keep recording a title strip a built app does not draw. It also explains why the Linux legs passed every time the hint moved: the hint was never in those builds. Measured rather than assumed, after two wrong turns: the macOS golden for graphics-draw-image-rect HAD its title before the Aqua flip and lost it in the flip itself, which is what showed the theme constant -- not the build hint -- was the thing deciding, and therefore that the port's own resolution had to consult it too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rees with Form From ParparVM Java Tests (Windows) run on b82867b, seeded from the x64 capture as the workflow asks (x64 and arm64 share this one directory). 145 of 172 tiles moved; 75 tests still pass unchanged. All 145 are the same cause: the legacy title area is no longer painted, because the port's getDesktopTitleBarMode() now consults the installed theme like Form.getDesktopTitleBarMode() always has, so Fluent's "native" finally reaches isNativeTitle(). 83 of them are a clean upward shift of 20-80px with the strip removed; the rest are dialogs and overlays, which reflow rather than translate -- every one of those starts differing at row ~16, inside the strip. Checked for the trap that caught ToolbarTheme, rather than assuming: nothing in the set went from content to blank. The five near-blank tiles were near-blank before (99.1, 99.1, 90.9, 84.9, 99.6 percent dominant) and only lost the strip's colours. MainActivity still shows "Hello Codename One" and its preview text; graphics-empty-clip still shows its green rectangles. And ToolbarTheme is NOT in this set at all -- it forces toolbar mode for its own capture, so it still draws the Toolbar it exists to photograph. Left to the arm64 leg to adjudicate: the two arches differ on 22 tiles, 19 of them by 1-5 pixels (anti-aliasing, within the comparator's default tolerance) and four LightweightPickerButtons tiles by 303 pixels, which is 0.07% of the frame. If any of those fail against this seed they will be named and handled rather than tolerated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From linux-build-run run on b82867b. 146 tiles per arch, 70 tests still passing, and the two arches reported identically (70/123/3 each), which is the check that this is the change and not one runner. Cause is the pair in b82867b: the port's getDesktopTitleBarMode() now consults the installed theme, and the suite's hand-written launcher sets desktop.titleBar the way a real build's stub does. Adwaita alone asks for "custom", so without that property these goldens kept a title strip that a built hellocodenameone -- which sets desktop.titleBar=native -- does not draw. That is also why every previous Linux leg passed while the hint moved: the hint was never in those builds. Checked for the trap that caught ToolbarTheme: no test lost its subject. FloatingActionButtonTheme and landscape cross a blankness threshold (91.9% to 99.5% and 92.1% to 99.7% dominant colour) purely because the strip was their remaining contrast -- the FAB is still drawn bottom-right and "Testing orientation lock..." still renders. ToolbarTheme is not in the set at all; it forces toolbar mode for its own capture and still photographs a Toolbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tually said The Linux x64 suite SIGSEGVed seven screenshots in, and the uploaded core answers where: signal 11, RIP 0x1d5b727, which llvm-symbolizer resolves against LinuxHelloMain.debug to java_util_HashMap.c inside com_codename1_ui_Display_getProperty. Walking the stack out of the core puts it on the EDT, under Cn1ssDeviceRunner.finalizeTest / runNextTest. So the EDT read Display.localProperties, passed the null check, and faulted inside get. That path is hot because of this branch: the port's getDesktopTitleBarMode() now reads desktop.titleBar, and nothing on these ports had ever called setProperty before, so getProperty had always taken the localProperties == null line and returned. The obvious conclusion is that the launcher published a plain HashMap from the main thread to a running EDT, and the reordering here removes that: setProperty before Display.init means no EDT exists to race with. It is free, it is strictly safer, and it is kept. It is NOT presented as the fix, because three things argue against it. The stub LinuxNativeBuilder generates has always set package_name after Display.init and ships that way. x86-64 is strongly ordered, so a store-then-load of the map pointer is visible. And this SIGSEGV has now hit two unrelated tests at unrelated points -- 7 screenshots in here, 138 in RealOsmVectorScreenshotTest on the previous run -- which is the signature of the intermittent crash linux-build-run already documents as undiagnosed across four occurrences, not of a deterministic bug in one path. So: the ordering is hygiene, the crash is still open, and it now has a core with a symbolized faulting frame, which is more than the four before it left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit moved setProperty ahead of Display.init to remove a suspected race. That is not allowed -- Display.init comes first -- and the reordering was poorly founded anyway, which the commit itself half-admitted: the generated stub has always set package_name after init, x86-64 is strongly ordered, and the SIGSEGV it was reacting to has hit unrelated tests 131 screenshots apart. So the property is set immediately after Display.init and before anything else, which is exactly where LinuxNativeBuilder and WindowsNativeBuilder put package_name, the hardening properties and the database switch. The launchers now match the stubs they stand in for, which is what they should have done from the start. The crash is unchanged by this and remains open, with the symbolized core from run 35587087114 as the first real evidence about it: EDT, inside Display.getProperty -> HashMap.get. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… reach Two reported defects, one commit because both are the same omission seen twice: these three themes were written for the light half and the widget set that existed at the time. CHAT. ios-modern and android-material each carry thirteen Chat UIIDs. Fluent, Aqua and Adwaita carried NONE -- not ChatBubbleUser, not even ChatView -- so a chat rendered as bare left- and right-aligned text with no bubble behind it, which is what ChatView_dark shows. All thirteen are added to each theme, light and dark. The user bubble is accent-bound so an app that retunes its accent retunes its own messages; the assistant bubble is a neutral platform surface, because a conversation with both sides in the accent colour reads as one voice. PALETTE. The accent override worked in light and did nothing in dark, and the reason was structural rather than a missed rule: the light rules bind through var(--accent-color, ...), which is what makes the compiler emit @cn1-bind:<UIID>.<key>=accent-color, and the dark blocks had ZERO var() in them -- 80, 80 and 66 hardcoded colours. ios-modern has always bound its dark block through --accent-color-dark, and all three of these themes already DECLARED that token; they just never used it. Bound by symmetry rather than by sweeping every colour: for each (selector, property) pair the light block binds to the accent, the dark block now binds the same pair wherever it declares it and the value is the dark accent token. That leaves Link, TextField.selected's focus ring and ListRendererFocus hardcoded in BOTH halves, which is the existing decision, not a new gap. Fluent and Adwaita come out with zero asymmetry; Aqua has one, FlatButton's #4DA3FF, which is a lightened accent with no token of its own -- binding it would repaint a shipped colour rather than make an override reach it, so it carries a comment saying so. The other two themes recompiled byte-identically in the same build, which is the check that nothing outside the desktop three moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as "the window dialog seems overly padded but I might have the
mechanism wrong". It is not padding: Dialog.showDialog lays a positionless
dialog out by percentages of its host --
showModal(h/100*20, h/100*10, w/100*20, w/100*20, ...)
-- 20% off the top, 10% off the bottom, 20% each side, so the dialog is always
70% x 60% of the window whatever is in it. The committed goldens are the
arithmetic: the same "Delete the document?" box is 215px tall at 400x300, 280px
at 1000x400 and 490px at 900x700, tracking the window and never the content.
The theme's Dialog padding of 4.2mm is a rounding error beside it. That is the
phone convention, where a dialog is a sheet that owns most of the screen.
On the desktop it now packs. showPacked is not new -- it is the path a dialog
already takes whenever a dialog position is set -- and it sizes from
contentPane.getPreferredW/H while honouring the dialogMaxWidthPercentInt and
dialogMaxWidthMMInt theme caps, so a long body still wraps into a centred card
instead of a full-width strip.
Placed in Dialog.showDialog rather than at the callers because both entry
points funnel through it: Form.showModal(reverse) calls showDialog(true,
reverse), and showModeless calls showDialog(false, false) whenever no dialog
position is set. Hosted and non-hosted desktop dialogs therefore meet here, and
fixing it here fixes both rather than the one the report happened to name.
Every desktop dialog golden moves as a result; those follow from CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DesktopNativeThemeParityTest.everyDesktopUiidHasADarkCounterpart failed on all three desktop themes, and its message is the diagnosis: cn1-derive is flattened against the LIGHT parent at compile time, so a UIID that derives only in the light block gets a concrete copy of the light colours and no $Dark entry at all -- it would keep its light colours on a dark form. ChatSendButton, ChatAttachButton and ChatVoiceButton derive from RaisedButton and Button and were declared in the light block only. Repeated in the dark block now, along with the derives for ChatInputField (TextField) and ChatBubbleText (Label), which had explicit dark colours but no derive and so were picking up the light parent's border and metrics. Verified against the compiled .res rather than the CSS, because the .res is what the test loads and the derive flattening only happens there: every one of the thirteen chat UIIDs that has a light key now has a $DarkUIID key in all three themes. Checked that this bites by reverting to the previous commit's themes, where the query returns exactly the three the test named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…that fit From ParparVM Java Tests (Windows) run 35619554916 on 1550d03. Eight tiles, 189 tests still passing, and the eight map one-to-one onto the three fixes with nothing else moving. ChatView and ChatInput, light and dark: bubbles, where there had been bare left- and right-aligned text on an unstyled form. The user's messages carry the accent and the assistant's a neutral surface. PaletteOverrideTheme_dark: the "Raised" button is magenta, which is what the label in the screenshot has always claimed -- "Magenta override active in both appearances". PaletteOverrideTheme_LIGHT is deliberately not in this set: light already worked, and the dark block was bound by symmetry with it rather than rewritten, so light must not move. It did not. The three Window-Dialog tiles are the clearest of the set. The same "Confirm / Delete the document?" box is now the SAME SIZE at 900x700 as at 1000x400, because it is sized by its content; before it was 465px and 280px, sized by its window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From the linux-build-run on 1550d03. Both arches reported identically -- 189 pass, 4 fail, the same eight tiles -- which is the check that this is the change and not one runner. Same three fixes as the Windows set, rendered by a different theme, which is worth having: Adwaita's chat bubbles carry its own #3584E4 accent rather than Fluent's, so the accent binding is exercised per-theme and not just once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From the scripts-macos run on 1550d03: 188 pass, 4 fail, the same eight tiles Windows and Linux reported. Three ports, three themes, one set of filenames -- which is the evidence that these three fixes are portable and scoped, not three coincidences. ChatView gains its bubbles (256 colours to 903 dark / 976 light, dominant colour down from 96.5% to roughly 73%, which is what adding surfaces to a flat field looks like). PaletteOverrideTheme_dark keeps its pixel count and changes its accent, because that is all the binding does. The dialogs shrink, so the scrim behind them grows: 56.0% to 94.7% at 900x700. Nothing in the set went near-blank, checked the same way as the earlier reseeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Makes the three desktop native themes actually native. PR #5845 built, measured and gated
them and deliberately switched nothing on; this turns them on and fills what turning them on
exposed.
The two bugs this found
commandBehavior: Nativewas a silent way to lose every command.MenuBar.updateCommandshands the commands to
setNativeCommandsand RETURNS without drawing soft buttons -- correctwhere there is a real menu bar, and on a platform without one it means they go to an empty
method and are never drawn at all. Nothing downstream can tell that from "the platform handled
it". Latent until a theme asked, which the desktop themes do, and which the Windows and Linux
ports cannot honour. Fixed in
setCommandBehavior, whereBUTTON_BAR->SOFTKEYalreadynormalises an unsupportable behaviour.
COVERAGE.md's Desktop section was the iOS section.3959d6886cset out to remove ablock the merge had duplicated and removed the wrong copy, so the file has had no record of
which desktop widgets are covered since. Restored from
2ceb4f0f9cand updated.What changed
Scrolling. All three themes derived their scrollbar from the mobile one --
DesktopScrollThumb { cn1-derive: ScrollThumb; }and a transparent track, so no gutter, nominimum length and no highlight -- while the mobile themes carried the full desktop
treatment. Each theme now sizes its own gutter from the platform's figure and gives the thumb
its hover and drag colours. Two things measured rather than assumed: the highlight states are
.selectedand.pressed, never.hover(a.hoverrule there compiles and is neverpainted), and
cn1-deriveemits the whole state family by copying the base, so the old stubproduced
sel#bgColoridentical tobgColor-- a highlight present in the resource andinvisible on screen. The four UIIDs are also seeded in
resetThemePropsnow, so a theme thatturns the constant on without defining them no longer draws an invisible bar that still
reserves its gutter.
Menus.
PopupContentPane,CommandList,Command,TouchCommand,Tooltip,TooltipDialogand the dialog command area were undefined in all three themes, so each fellthrough to
UIManager's blank default -- black on white, on a dark window. Plus a realright-click menu:
Component.addContextMenuListenerhas fired for years and nothing everopened one.
Dialogs.
defaultNativeWindowModeBoolon the three themes, so a dialog opens as a realoperating system window. Anchored popups never do.
Keyboard. Tab and Shift-Tab move focus; Escape cancels. The traversal machinery is old and
could not be wired to Tab as it stood: its filter is opt-in,
preferredTabIndexdefaults to-1 and
TextAreais the only class in the framework that opts in, so Tab would have walkedbetween a form's text fields and skipped every button between them. The desktop order is its
own, filtered on focusability. Escape resolves through one
Dialog.cancel()that the windowclose control and the back gesture already meant separately.
desktopTitleBarModegained a reader. The three themes have carried the constant sincethey landed and nothing read it.
Three new components.
Separator,GroupBox,Stepper-- every desktop toolkit has allthree and Codename One had none.
The flip. Windows -> Fluent, Linux -> Adwaita, macOS -> Aqua. Java SE stays on legacy
deliberately: that default reaches every desktop application ever built rather than only ours.
Fidelity: 9 scored controls to 22. Everything that makes a window look like a desktop
window was unmeasured, including the interactive scrollbar. Three rows are not scored on every
platform, and that is the honest answer rather than a gap -- an
NSScrollerrenders nothingthrough
cacheDisplay(measured: correctusableParts, correct frame,isHidden=false, onecolour in the tile, tried detached and inside a real
NSScrollViewin both styles),NSMenubelongs to the window server, and the AppKit and GTK tooltips are separate windows.
What is still open
screenshot baselines. Both are captured from the runners that score them, never locally, so
those legs are red until the capture workflows have run and their output has been reviewed.
toolchain exists on the machine this was written on. CI is the first verifier.
setNativeCommands, sotheir commands stay in the now-properly-themed Toolbar, and
isDesktopHideToolbar()will nothide the only place they are drawn. Tracked in
COVERAGE.md.7294 core unit tests green. SpotBugs zero findings on
core-unittests; cast-semantics,control-characters, copyright, build-hint-catalog and the four fidelity gates clean. Vale and
LanguageTool clean on both guide chapters.
🤖 Generated with Claude Code